package il.ac.bgu.cs.heuristics.graph.pddl.parser;

import il.ac.bgu.cs.heuristics.graph.pddl.parser.types.AbstractParameter;
import il.ac.bgu.cs.heuristics.graph.pddl.parser.types.ExpressionBuilder;
import il.ac.bgu.cs.heuristics.graph.pddl.parser.types.LogicalExpression;
import il.ac.bgu.cs.heuristics.graph.pddl.parser.types.LogicalOperator;
import il.ac.bgu.cs.heuristics.graph.pddl.parser.types.Predicate;
import il.ac.bgu.cs.heuristics.graph.pddl.parser.types.Type;
import il.ac.bgu.cs.heuristics.utils.CollectionUtils;
import il.ac.bgu.cs.heuristics.utils.Validation;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

/**
 * A parser for PDDL elements
 * 
 * @author Ronen Pinko
 * 
 */
public class ExpressionParser { // NOPMD too many methods

	private ExpressionParser() {
	} // no instantiation

	public static AbstractParameter parseTypedParameter(String s) {
		try {
			final List<Token> tokens = tokenize(new StringReader(s));
			final ConsumingIterable<Token> tokenConsumingIterable = new ConsumingIterable<Token>(
					tokens);
			final List<AbstractParameter> paramContainer = CollectionUtils
					.newArrayList();
			new TypedParameterParseStep(tokenConsumingIterable, paramContainer)
					.process();
			return paramContainer.get(0);
		} catch (IOException e) {
			throw new IllegalArgumentException("string '" + s
					+ "' can't be parsed", e);
		}
	}

	public static LogicalExpression parseLogicalExpression(String s)
			throws IllegalArgumentException {
		try {
			final LogicalExpression.Builder builder = new LogicalExpression.Builder();
			final List<Token> tokens = tokenize(new StringReader(s));
			final ConsumingIterable<Token> tokenConsumingIterable = new ConsumingIterable<Token>(
					tokens);
			new OperatorParseStep(tokenConsumingIterable, builder, new IStep() {

				@Override
				public void process() {
					while (!tokens.isEmpty()) {
						builder.addPredicate(parsePredicate(tokens));
						final ConsumingIterator<Token> tokenConsumingIt = (ConsumingIterator<Token>) tokenConsumingIterable
								.iterator();
						if (tokenConsumingIt.hasNext()
								&& tokenConsumingIt.peek() == Token.LEFT_PAREN) {
							break;
						}
					}
				}
			}).process();

			return builder.build();
		} catch (IOException e) {
			throw new IllegalArgumentException("string '" + s
					+ "' can't be parsed", e);
		}
	}

	public static Predicate parsePredicate(String s) {
		try {
			return parsePredicate(tokenize(new StringReader(s)));
		} catch (IOException e) {
			throw new IllegalArgumentException("string '" + s
					+ "' can't be parsed", e);
		}
	}

	private static Predicate parsePredicate(final List<Token> tokens) {
		final ConsumingIterable<Token> tokenConsumingIterable = new ConsumingIterable<Token>(
				tokens);
		final Predicate.Builder builder = new Predicate.Builder();
		new OperatorParseStep(tokenConsumingIterable, builder,
				new PredicateParseStep(tokenConsumingIterable, builder))
				.process();
		return builder.build();
	}

	private static List<Token> tokenize(Reader stream) throws IOException {
		final List<Token> tokens = CollectionUtils.newArrayList();
		final StringBuilder buffer = new StringBuilder();
		int i;
		while ((i = stream.read()) >= 0) { // NOPMD
			final char c = (char) i;
			final Token constToken = Token.create(Character.toString(c));
			if (Token.RIGHT_PAREN.equals(constToken)
					|| Token.LEFT_PAREN.equals(constToken)
					|| (Token.DASH.equals(constToken) && buffer.length() == 0)) {
				createTokenIfBufferNotEmpty(tokens, buffer);
				tokens.add(constToken);
			} else if (c == ' ') {
				createTokenIfBufferNotEmpty(tokens, buffer);
			} else {
				buffer.append(c);
			}
		}
		createTokenIfBufferNotEmpty(tokens, buffer);

		return tokens;
	}

	private static void createTokenIfBufferNotEmpty(final List<Token> tokens,
			final StringBuilder buffer) {
		if (buffer.length() > 0) {
			tokens.add(Token.create(buffer.toString()));
			clearBuffer(buffer);
		}
	}

	private static void clearBuffer(StringBuilder sb) {
		sb.delete(0, Integer.MAX_VALUE);
	}

	private interface IStep {
		void process();
	}

	private static abstract class AbstractParseStep implements IStep {
		final ConsumingIterable<Token>	tokensConsumingIterable;

		AbstractParseStep(ConsumingIterable<Token> tokensConsumingIterable) {
			this.tokensConsumingIterable = tokensConsumingIterable;
		}

	}

	private static abstract class AbstractParenthesisBalancedParseStep extends
			AbstractParseStep {

		AbstractParenthesisBalancedParseStep(
				ConsumingIterable<Token> tokensConsumingIterable) {
			super(tokensConsumingIterable);
		}

		@Override
		public final void process() {
			ConsumingIterator<Token> tokenConsumingIt = (ConsumingIterator<Token>) tokensConsumingIterable
					.iterator();
			if (tokenConsumingIt.hasNext()) {
				if (Token.RIGHT_PAREN.equals(tokenConsumingIt.next())) {
					if (step()) {
						tokenConsumingIt = (ConsumingIterator<Token>) tokensConsumingIterable
								.iterator();
						if (tokenConsumingIt.hasNext()
								&& !Token.LEFT_PAREN.equals(tokenConsumingIt
										.next())) {
							throw new IllegalArgumentException(
									"expecting close parenthesis - "
											+ tokensConsumingIterable);
						}
					}
				} else {
					throw new IllegalArgumentException(
							"expecting open parenthesis - "
									+ tokensConsumingIterable);
				}
			} else {
				throw new IllegalArgumentException(
						"can't parse, empty token list");
			}

		}

		/**
		 * 
		 * @return {@code true} if parse step successful
		 */
		protected abstract boolean step();

	}

	private final static class OperatorParseStep extends
			AbstractParenthesisBalancedParseStep {

		private final ExpressionBuilder	builder;
		private final IStep				nextStep;

		OperatorParseStep(ConsumingIterable<Token> tokens,
				ExpressionBuilder builder, IStep nextStep) {
			super(tokens);
			this.builder = builder;
			this.nextStep = nextStep;
		}

		@Override
		protected boolean step() {
			final ConsumingIterator<Token> tokenConsumingIt = (ConsumingIterator<Token>) tokensConsumingIterable
					.iterator();
			boolean foundOperator = true;
			if (tokenConsumingIt.hasNext()) {
				final Token next = tokenConsumingIt.peek();
				if (Token.OP_AND.equals(next)) {
					builder.operator(LogicalOperator.AND);
				} else if (Token.OP_NOT.equals(next)) {
					builder.operator(LogicalOperator.NOT);
				} else if (Token.OP_OR.equals(next)) {
					builder.operator(LogicalOperator.OR);
				} else {
					foundOperator = false;
				}
			}
			if (foundOperator) {
				tokenConsumingIt.consumePeeked();
			} else {
				tokensConsumingIterable.returnElement(Token.RIGHT_PAREN);
			}
			nextStep.process();
			return foundOperator;
		}
	}

	private final static class PredicateParseStep extends
			AbstractParenthesisBalancedParseStep {

		private final Predicate.Builder	builder;

		PredicateParseStep(ConsumingIterable<Token> tokens,
				Predicate.Builder builder) {
			super(tokens);
			this.builder = builder;
		}

		@Override
		public boolean step() {
			final ConsumingIterator<Token> tokenConsumingIt = (ConsumingIterator<Token>) tokensConsumingIterable
					.iterator();
			if (tokenConsumingIt.hasNext()) {
				final Token nextToken = tokenConsumingIt.next();
				if (nextToken.isConstant()) {
					throw new IllegalArgumentException(
							"illegal predicate name - '" + nextToken + "'");
				}
				builder.name(nextToken.value);
				while (tokenConsumingIt.hasNext()) {
					final ConsumingIterator<Token> paramConsumingIt = (ConsumingIterator<Token>) tokensConsumingIterable
							.iterator();
					if (Token.LEFT_PAREN.equals(paramConsumingIt.peek())) {
						return true;
					}
					new PredicateParameterParseStep(tokensConsumingIterable,
							builder).process();
				}
				throw new IllegalArgumentException(
						"predicate should end with left parenthesis");

			} else {
				throw new IllegalArgumentException("missing predicate name");
			}

		}

	}

	private static class TypedParameterParseStep extends AbstractParseStep {

		protected final List<AbstractParameter>	paramContainer;

		TypedParameterParseStep(
				ConsumingIterable<Token> tokensConsumingIterable,
				List<AbstractParameter> paramContainer) {
			super(tokensConsumingIterable);
			this.paramContainer = paramContainer;
		}

		private Set<Type> processPossibleTypes() {
			final ConsumingIterator<Token> tokenConsumingIt = (ConsumingIterator<Token>) tokensConsumingIterable
					.iterator();
			if (tokenConsumingIt.hasNext()) {
				final Token typeIndicator = tokenConsumingIt.peek();
				if (Token.DASH.equals(typeIndicator)) {
					tokenConsumingIt.consumePeeked();
					return handleTypes();
				} else {
					throw new IllegalArgumentException(
							"expecting type delimiter - '-'");
				}
			} else {
				throw new IllegalArgumentException(
						"predicate parameter must have type");
			}
		}

		private Set<Type> handleTypes() {
			final ConsumingIterator<Token> tokenConsumingIt = (ConsumingIterator<Token>) tokensConsumingIterable
					.iterator();
			if (tokenConsumingIt.hasNext()) {
				final Token typeOrParen = tokenConsumingIt.peek();
				if (Token.RIGHT_PAREN.equals(typeOrParen)) {
					return handleComplextTypes();
				} else {
					return handleSimpleType();
				}
			} else {
				throw new IllegalArgumentException(
						"expecting type name after dash");
			}
		}

		private Set<Type> handleSimpleType() {
			final ConsumingIterator<Token> tokenConsumingIt = (ConsumingIterator<Token>) tokensConsumingIterable
					.iterator();
			final Token typeToken = tokenConsumingIt.next();
			if (typeToken.isConstant()) {
				throw new IllegalArgumentException(
						"expecting type name, got - '" + typeToken + "'");
			}
			return CollectionUtils.returnAsSet(Type.from(typeToken.value));
		}

		private Set<Type> handleComplextTypes() {
			final Set<Type> possibleTypes = CollectionUtils.newLinkedHashSet();
			new AbstractParenthesisBalancedParseStep(tokensConsumingIterable) {

				@Override
				protected boolean step() {
					final ConsumingIterator<Token> tokenConsumingIt2 = (ConsumingIterator<Token>) tokensConsumingIterable
							.iterator();
					if (tokenConsumingIt2.hasNext()) {
						final Token either = tokenConsumingIt2.next();
						if (Token.EITHER.equals(either)) {
							handleMultipleTypes(possibleTypes,
									tokenConsumingIt2);
						} else {
							throw new IllegalArgumentException(
									"expecting 'either', got - '" + either
											+ "'");
						}
					} else {
						throw new IllegalArgumentException("expecting 'either'");
					}

					return true;
				}

				private void handleMultipleTypes(final Set<Type> possibleTypes,
						final ConsumingIterator<Token> tokenConsumingIt2) {
					while (tokenConsumingIt2.hasNext()) {
						final Token nextToken = tokenConsumingIt2.peek();
						if (Token.LEFT_PAREN.equals(nextToken)) {
							return;
						} else if (nextToken.isConstant()) {
							throw new IllegalArgumentException(
									"expecting type name, got - '" + nextToken
											+ "'");
						} else {
							tokenConsumingIt2.consumePeeked();
							possibleTypes.add(Type.from(nextToken.value));
						}
					}
				}
			}.process();
			return possibleTypes;
		}

		@Override
		public void process() {
			final ConsumingIterator<Token> tokenConsumingIt = (ConsumingIterator<Token>) tokensConsumingIterable
					.iterator();
			if (tokenConsumingIt.hasNext()) {
				final Token paramNameToken = tokenConsumingIt.next();
				if (paramNameToken.isConstant()) {
					throw new IllegalArgumentException(
							"illegal predicate var name - '" + paramNameToken
									+ "'");
				} else {
					final String paramName = paramNameToken.value;
					final Set<Type> types = processPossibleTypes();
					paramContainer.add(AbstractParameter.newInstance(paramName,
							types, paramName.charAt(0) == '?'));
				}

			}

		}

	}

	private final static class PredicateParameterParseStep extends
			TypedParameterParseStep {

		private final Predicate.Builder	builder;

		PredicateParameterParseStep(ConsumingIterable<Token> tokens,
				Predicate.Builder builder) {
			super(tokens, new ArrayList<AbstractParameter>());
			this.builder = builder;
		}

		@Override
		public void process() {
			super.process();
			builder.addVariable(paramContainer.get(0));
		}

	}

	/**
	 * A wrapper for iterables which can be iterated using
	 * {@link ConsumingIterator} This means that iterated elements will be
	 * removed from the iterable.
	 * 
	 * Supports returning an element already consumed
	 * 
	 * @author Ronen Pinko
	 * 
	 * @param <E>
	 *            underlying element type
	 */
	private static final class ConsumingIterable<E> implements Iterable<E> {

		private final List<E>	backingList;

		ConsumingIterable(List<E> backingIterable) {
			this.backingList = backingIterable;

		}

		@Override
		public Iterator<E> iterator() {
			return new ConsumingIterator<E>(backingList.iterator());
		}

		@Override
		public String toString() {
			return backingList.toString();
		}

		public void returnElement(E e) {
			backingList.add(0, e);
		}

	}

	/**
	 * A wrapper for iterators which removes an element of the backing
	 * {@link Iterable} when {@link Iterator#next()} is called
	 * 
	 * @author Ronen Pinko
	 * 
	 * @param <E>
	 *            underlying element type
	 */
	private static final class ConsumingIterator<E> implements Iterator<E> {

		private final Iterator<E>	backingIterator;

		ConsumingIterator(Iterator<E> backingIterator) {
			this.backingIterator = backingIterator;
		}

		@Override
		public boolean hasNext() {
			return backingIterator.hasNext();
		}

		@Override
		public E next() {
			final E next = backingIterator.next();
			backingIterator.remove();
			return next;
		}

		/**
		 * @return next element without consuming it from the backing iterable
		 */
		public E peek() {
			return backingIterator.next();
		}

		@Override
		public void remove() {
			throw new UnsupportedOperationException(
					"consuming iterator doesn't support 'remove', use 'consumePeeked' instead");
		}

		public void consumePeeked() {
			backingIterator.remove();
		}

	}

	private static final class Token {

		private final static Token		OP_NOT		= new Token("not");
		private final static Token		OP_AND		= new Token("and");
		private final static Token		OP_OR		= new Token("or");
		private final static Token		RIGHT_PAREN	= new Token("(");
		private final static Token		LEFT_PAREN	= new Token(")");
		private final static Token		DASH		= new Token("-");
		private final static Token		EITHER		= new Token("either");

		private final static Set<Token>	CONSTANTS	= CollectionUtils
															.unmodifiableSet(
																	OP_NOT,
																	OP_AND,
																	OP_OR,
																	RIGHT_PAREN,
																	LEFT_PAREN,
																	DASH,
																	EITHER);

		private final String			value;

		private Token(String value) {
			this.value = value;
		}

		static Token create(String s) {
			Validation.ensureArgumentContainsText(s);
			for (Token token : CONSTANTS) {
				if (token.value.equalsIgnoreCase(s)) {
					return token;
				}
			}
			return new Token(s);
		}

		public boolean isConstant() {
			for (Token constant : CONSTANTS) {
				if (this.equals(constant)) {
					return true;
				}
			}
			return false;
		}

		@Override
		public String toString() {
			return "Tok(\"" + value + "\")";
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((value == null) ? 0 : value.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj) {
				return true;
			}
			if (obj == null) {
				return false;
			}
			if (getClass() != obj.getClass()) {
				return false;
			}
			final Token other = (Token) obj;
			if (value == null) {
				if (other.value != null) {
					return false;
				}
			} else if (!value.equals(other.value)) {
				return false;
			}
			return true;
		}

	}

}
